package tp.pr4;

import tp.pr4.Interpreter;
import tp.pr4.instructions.Instruction;
import tp.pr4.instructions.UndoManager;
import tp.pr4.instructions.exceptions.InstructionExecutionException;
import tp.pr4.instructions.exceptions.WrongInstructionFormatException;
import tp.pr4.items.Item;
import tp.pr4.items.ItemContainer;
import tp.pr4.lang.Messages;
import tp.pr4.listeners.AbstractNavigationModuleListener;
import tp.pr4.listeners.ItemContainerListener;
import tp.pr4.listeners.RobotListener;
import tp.pr4.listeners.RobotListenerArray;
import tp.pr4.sound.Sound;

public class RobotEngine {
	//constantes numéricas
	private static final int FUEL_INITIAL = 100;
	//private static final int FUEL_MAX = 9001;
	private static final int POINTS_INITIAL = 0;
	
	private RobotListenerArray listeners = new RobotListenerArray();

	private Integer fuel = 0;
	private Integer points = 0;

	private ItemContainer pocket;
	private NavigationModule gps;
	
	private UndoManager undoManager;
	
	private boolean endSimulation = false;
	private boolean win = false;
	
	
	/**
	 * 
	 * @param initialPlace sitio desde donde se parte
	 * @param direction	dirección inicial
	 * @param cityMap mapa de la ciudad
	 */
	public RobotEngine (City cityMap, Place initialPlace, Direction direction) {
		this.fuel = FUEL_INITIAL;
		this.points = POINTS_INITIAL;
		this.pocket = new ItemContainer();
		this.gps = new NavigationModule(cityMap, initialPlace);
		this.undoManager = new UndoManager();
		this.pocket.addListener(new ItemContainerListener() {

			@Override
			public void onChange(Item it, ItemContainerChangeType type) {
				listeners.onInventoryChanged(pocket, gps.getCurrentPlace());
				if (type == ItemContainerChangeType.ITEM_PICKED)
					Sound.pickup.play();
				else
					Sound.monsterHurt.play();
			}
			
		});
		
		this.gps.addListener(new AbstractNavigationModuleListener() {
			@Override
			public void onSpaceShipReached() {
				requestWin();
			}
		});
	}
	
	/**
	 * The robot says a message
	 * @param message
	 */
	public void robotSays(String message) {
		System.out.println(Messages.ROBOT_NAME + Messages.OUTPUT_SAYS + message);
	}
	/**
	 * The robot says a message (on system.err)
	 * @param message
	 */
	public void robotErrs(String message) {
		//System.err.println(Messages.ROBOT_NAME + Messages.OUTPUT_SAYS + message);
		System.out.println(Messages.ROBOT_NAME + Messages.OUTPUT_SAYS + message);
	}
	/**
	 * Prints both the fuel level and recycled material amount
	 */
	public void printRobotState () { //pública porque la necesitan el fuel y el garbage al usarse
		System.out.println(Messages.OUTPUT_SAYFUEL + this.fuel + Interpreter.LINE_SEPARATOR + Messages.OUTPUT_SAYRM + this.points);
	}
	

	/**
	 * Adds an amount of fuel to the robot (it can be negative)
	 * @param fuel Amount of fuel added to the robot
	 */
	public void addFuel(int fuel) {
		this.fuel += fuel;
		if (this.fuel < 0) 
			this.fuel = 0;
		this.listeners.onFuelChanged(this.fuel);
		if (this.fuel == 0){
			this.requestQuit();
			if (!this.win) {
				Sound.playerDeath.play();
				this.listeners.onDeath();
			}
		}
//		if (this.fuel > FUEL_MAX)
//		 	this.fuel = FUEL_MAX;
	}
	/**
	 * Adds an amount of fuel to the robot (it can be negative)
	 * @param weight Amount of fuel added to the robot
	 */
	public void addRecycledMaterial (int weight) {
		if (weight == 0)
			return;
		points += weight;
		this.listeners.onPointsChanged(this.points);
	}
	
	/**
	 * for testing
	 * @return current fuel level
	 */
	public int getFuel() {
		return this.fuel;
	}	
	/**
	 * for testing
	 * @return current engine points
	 */
	public int getRecycledMaterial() {
		return this.points;
	}
	
	/**
	 * prints all info related to the robot status after a fuel consumption
	 * @param consumedFuel fuel that was consumed
	 */
	public void sayAndUpdateDataAfterFuelConsumingAction(int consumedFuel) {
		System.out.println(this.gps.getCurrentHeading().sayDirection());
		this.addFuel(consumedFuel);
		this.printRobotState();
	}	
	
	/**
	 * It executes an instruction. The instruction must be configured with the context before executing it. It controls the end of the simulation. If the execution of the instruction throws an exception, then the corresponding message is printed <br>
	 * It also adds the instruction to the undo manager (if the execution was correct), meaning it will be the next undid if the undo instruction is executed, overwriting any previous.
	 * @param c The instruction to be executed
	 */
	public void communicateRobot(Instruction c) {
		c.configureContext(this, this.gps, this.pocket);
		if (this.executeInstruction(c) && c.isUndoable()) 
			this.undoManager.addInstruction(c);	
	}
	/**
	 * Executes an instruction
	 * @param c instruction to be executed
	 * @return true if the instruction did not fail, false otherwise
	 */
	public boolean executeInstruction(Instruction c) {
		try {
			c.execute();
			return true;
		} catch (InstructionExecutionException ex) {
			this.robotErrs(ex.getMessage());
			this.listeners.onInstructionFailed(c);
			return false;
		}
	}
	/**
	 * Undoes the last executed instruction, or does nothing if there was no such instruction
	 */
	public boolean undoInstruction() {
		try {
			return this.undoManager.undoLastInstruction();
		} catch (InstructionExecutionException e) {
			return false;
		}
	}
	/**
	 * Redoes the last executed instruction, or does nothing if there was no such instruction
	 */
	public boolean redoInstruction() {
		try {
			this.executeInstruction(this.undoManager.getLastUndidInstruction());
			return true;
		} catch (IndexOutOfBoundsException e) {
			return false;
		}
	}

	/**
	 * Requests the end of the simulation
	 */
	public void requestQuit() {
		this.endSimulation = true;
	}
	/**
	 * Prints the information about all possible instructions
	 */
	public void requestHelp() {
		System.out.println(Interpreter.interpreterHelp());
	}
	/**
	 * Sets win to true
	 */
	public void requestWin() {
		this.win  = true;
	}
	
	/**
	 * It starts the robot engine. 
	 * Basically, it reads a line, the interpreter generates the corresponding instruction and executes it. 
	 * The simulation finishes when the robot arrives at the space ship, the user types "QUIT", or the robot runs out of fuel
	 * @param hasInput true if the robot needs to wait for console input, false if it receives it otherwise
	 */
	public void startEngine(){
		//inicializamos el interpreter, el escáner, y mostramos los datos de lugar de incio del robot
		java.util.Scanner sc = new java.util.Scanner(System.in);
		System.out.println(this.gps.getCurrentPlace().toString());
		System.out.println(this.gps.getCurrentHeading().sayDirection());
		this.printRobotState();

		while (!this.endSimulation) {
			System.out.print(Messages.PROMPT);
			try {
				Instruction instruction = Interpreter.generateInstruction(sc.nextLine()); //ya que ignoramos mayus y minus
				this.communicateRobot(instruction);
			} catch (WrongInstructionFormatException ex) {
				System.err.println(ex.getMessage());
			}
		}		

		sc.close();
	}
	
	/**
	 * Getter for the robot's navigationModule
	 * @return The robot's NavigationModule
	 */
	public NavigationModule getNavigationModule () {
		return this.gps;
	}
	
	/**
	 * Getter for the robot's itemContainer
	 * @return The robot's Pocket
	 */
	public ItemContainer getPocket() {
		return this.pocket;
	}
	
	/**
	 * Calls all listeners when an object is used
	 * @param item
	 * @param currentHeading
	 */
	public void hasUsedItem(Item item, Direction currentHeading) {
		this.listeners.onItemUsed(item, currentHeading);
		Sound.useditem.play();
	}
	
	/**
	 * Returns true if the robot has any fuel left
	 * @return 
	 */
	public boolean hasFuel() {
		return this.fuel != 0;
	}
	
	/**
	 * adds Robot Listener to this robot. The methods will be called according to what is specified in RobotListener.java
	 */
	public void addRobotListener(RobotListener r) {
		this.listeners.add(r);
	}


	
}
